package simtester.server

import grails.util.Environment
import java.util.concurrent.LinkedBlockingQueue
import org.springframework.beans.factory.InitializingBean
import simtester.cloud.Cloud
import simtester.server.TestCase;
import simtester.server.TestRun;

class CloudService implements InitializingBean {
    def grailsApplication
    def cloud
    def creationThread
    def removalThread
    static transactional = false
    
    static usersPerInstance = 25
    //static usersPerInstance = 2
    
    void afterPropertiesSet() {
        def config = grailsApplication.config
        cloud = new Cloud(type:     config.test.cloud,
                          env:      config.test.env,
                          creds:    config.aws.credentialsfile,
                          endpoint: config.aws.endpoint,
                          key:      config.aws.key)
    }
    
    /**
     * Runs the test agent. Accepts a map of named parameters
     * @param testcaseId Required. ID of TestCase
     * @param repeats Optional. Default = 1. Number of times to repeat the test.
     * @param duration Optional. Amount of time in milliseconds to run the test. Overrides "repeats" option
     */
    void runTestAgent(args) {
        def repeats = args.repeats
        if (!args.duration) {
            repeats = args.repeats ?: 1
        }
        
        def tr = new TestRun(status: 'running', repeats: repeats, testDuration: args.duration, name: args.testrunName)
        
        def instancesInfo
        // For test suite
        if (args.testsuiteId) {
            def ts = TestSuite.get(args.testsuiteId)
            def usersPerTest = args.users
            tr.testsuite = ts
            tr.usersPerTest = usersPerTest
            tr.save(failOnError:true)

            instancesInfo = calculateNumberOfInstancesForTestSuite(usersPerTest)
        // For test case
        } else {
            def tc = TestCase.get(args.testcaseId)
            def users = args.users ?: 1
            tr.testcase = tc
            tr.users = users
            tr.save()
            
            instancesInfo = calculateNumberOfInstances(users)
            instancesInfo.each {
                it.script = tc.script
            }
        }
        
        runAgent(repeats: repeats,
                 duration: args.duration,
                 users: args.users,
                 testrun: tr,
                 instancesInfo: instancesInfo)
    }
    
    void runLoadTestAgent(args) {
        def tr = new TestRun(status: 'running', name: args.testrunName, type: 'Load')
        
        def ts = TestSuite.get(args.testsuiteId.toLong())
        tr.testsuite = ts
        tr.testcaseIds = ts.testcases.collect { it.id }
        tr.testDuration = getOverallDuration(args.testcases)
        tr.save(failOnError:true)
        
        def instancesInfo
        instancesInfo = calculateInstancesForLoadTestSuite(args.testcases)

        runAgent(testrun: tr,
                 instancesInfo: instancesInfo)
    }

    void runAgent(args) {
        def agentPackage = grailsApplication.parentContext.getResource('classpath:resources/agent.jar').getFile().canonicalPath
        agentPackage = new File(agentPackage)

        def instances = Instance.findAllByStatus('free', [max: args.instancesInfo.size()])
        if (instances.size() == args.instancesInfo.size()) {
            instances.each {
                it.status = 'busy'
                it.save(flush:true, failOnError:true)
            }
        }

        creationThread = Thread.start {
            def threads = []
            def exceptions = new LinkedBlockingQueue()
            if (instances.size() == args.instancesInfo.size()) {
                args.instancesInfo.eachWithIndex { x, i ->
                    threads << Thread.start {
                        try {
                            def agentArgs = [
                                instanceId: instances[i].cloudId,
                                testcaseId: x.testcaseId,
                                script:     x.script,
                                firstuser:  x.firstuser,
                                url:        "${grailsApplication.config.grails.serverURL}/",
                                testrunId:  args.testrun.id
                            ]
                            
                            // GUI test
                            if (x.users) {
                                agentArgs.with {
                                    repeats   = args.repeats
                                    duration  = args.duration
                                    users     = x.users
                                }
                            // Load test
                            } else {
                                agentArgs.with {
                                    peruser   = x.peruser
                                    per       = x.per
                                    duration  = x.duration
                                    delay     = x.delay
                                    totalduration = x.totalduration
                                }
                            }
                            
                            //println "agentArgs: $agentArgs"
                            
                            cloud.setupTestAgent(agentArgs)
                        } catch (Exception e) {
                            println "Exception: $e"
                            e.printStackTrace()
                            exceptions << e
                        }
                    }
                }
            } else {
                instances = []
                args.instancesInfo.each { inst ->
                    threads << Thread.start {
                        try {
                            def agentArgs = [
                                testcaseId: inst.testcaseId,
                                script:     inst.script,
                                agentpkg:   agentPackage,
                                firstuser:  inst.firstuser,
                                url:        "${grailsApplication.config.grails.serverURL}/",
                                testrunId:  args.testrun.id
                            ]
                            
                            // GUI test
                            if (inst.users) {
                                agentArgs.with {
                                    repeats   = args.repeats
                                    duration  = args.duration
                                    users     = inst.users
                                }
                            // Load test
                            } else {
                                agentArgs.with {
                                    peruser = inst.peruser
                                    per = inst.per
                                    duration = inst.duration
                                    delay = inst.delay
                                    totalduration = inst.totalduration
                                }
                            }
                            
                            def instance = cloud.createTestAgent(agentArgs)

                            instances << new Instance(cloudId: instance.id, amiId: instance.amiId, type: instance.type, status: 'busy').save(flush:true, failOnError:true)
                        } catch (Exception e) {
                            exceptions << e
                        }
                    }
                }
            }
            
            threads.each { it.join() }

            if (exceptions.size() == 0) {
                instances.each { args.testrun.addToInstances(it) }
                args.testrun.save(flush: true, failOnError: true)
            } else {
                //TODO: Ending the TestRun this way does not work
                exceptions.size().times {
                    println "exception"
                    args.testrun.log("Encountered error that caused test to fail: " + exceptions.take().message)
                }
                args.testrun.status = 'failed'
                args.testrun.save()

                instances.each {
                    it.status = 'free'
                    it.save()
                }

                scheduleRemovalOfInstances(args.testrun)
            }
        }
    }

    void scheduleRemovalOfInstances(TestRun testrun) {
        removalThread = Thread.start {
            testrun.instances.each {
                it.status = 'free'
                it.save(flush:true, failOnError:true)
            }

            if (testrun.instances && !testrun.instances.isEmpty()) {
                testrun.instances.clear()
            }
        }
    }
    
    private def calculateNumberOfInstances(users) {
        def instancesCount = Math.ceil(users / usersPerInstance.toFloat())
        
        def instances = []
        instancesCount.times {
            instances << [firstuser: it*usersPerInstance + 1, users: usersPerInstance]
        }
        
        if (users % usersPerInstance > 0) {
            instances[-1].users = users % usersPerInstance
        }
        
        return instances
    }
    
    private def calculateNumberOfInstancesForTestSuite(usersPerTest) {
        def instances = []
        usersPerTest.each { tcId, numUsers ->
            def tcInstances = calculateNumberOfInstances(numUsers)
            
            def tc = TestCase.get(tcId.toLong())
            tcInstances.each {
                it.testcaseId = tcId.toLong()
                it.script = tc.script
            }
            instances += tcInstances
        }
        return instances
    }
    
    private def calculateInstancesForLoadTestSuite(testcases) {
        def instances = []
        testcases.each { tc ->
            def script = TestCase.get(tc.id).script
            
            // Total duration
            long total = 0
            tc.phases.each { phase ->
                total += phase.duration
            }
            
            long delayCounter = 0
            int userCounter = 1
            tc.phases.each { phase ->
                def info = [:]
                info.testcaseId = tc.id
                info.script = script
                
                info.peruser = phase.users
                info.per = phase.per
                info.duration = phase.duration
                
                info.delay = delayCounter
                info.totalduration = total
                info.firstuser = userCounter
                
                if (phase.users != 0) {
                    instances << info
    
                    int truncated = (info.duration - 1) / info.per
                    userCounter += truncated * info.peruser + info.peruser
                }
                
                delayCounter += phase.duration
            }
        }
        return instances
    }
    
    private getOverallDuration(testcases) {
        def durations = []
        testcases.each { tc ->
            // Total duration
            long total = 0
            tc.phases.each { phase ->
                total += phase.duration
            }
            durations << total
        }
        
        return durations.max()
    }
}
